/* Copyright (c) 2009 Mark L. Howison
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  (1) Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 *  (2) Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *  (3) The name of the copyright holder may not be used to endorse or promote
 *      products derived from this software without specific prior written
 *      permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package edu.berkeley.edrl.wiikinemathics;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.logging.Logger;
import javax.swing.JPanel;

/**
 *
 * @author mhowison
 */
public class MotionPanel extends JPanel {

    private final static Color TARGET_COLOR = Color.BLACK;
    private final static Color TRACKER_COLOR = Color.BLACK;
    private final static Color WAYPOINT_COLOR = Color.BLACK;
    private final static Color[] HEATMAP_COLORS = {
        Color.GREEN,
        Color.YELLOW,
        Color.RED
    };

    private double trackerLeft, trackerRight, targetLeft, targetRight;
    double ratio;
    private int width, height, radius, margin;
    private int targetAlpha = 255;
    private String message = "";
    private Font font;
    private BasicStroke dashedStroke;
    private BasicStroke lightStroke;
    private BasicStroke heavyStroke;
    private BasicStroke[] waypointStrokes;
    private ArrayList<Double> waypointsRight = new ArrayList(16);
    private ArrayList<Double> waypointsLeft = new ArrayList(16);

    /** Creates new form MotionDisplay */
    public MotionPanel() {
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        radius = (int)(getHeight() / 80.0);
        margin = 2*radius;
        width = getWidth() - 2*margin;
        height = getHeight() - 2*margin;
        ratio = WiiKinemathicsApp.getControls().getRatio();
        int xLeft  = margin + (int)(0.2*width);
        int xRight = margin + (int)(0.8*width);

        float dash = (float)Math.ceil(height / 800.0);
        float[] dashes = { 2*dash, 2*dash };
        dashedStroke = new BasicStroke(dash,BasicStroke.CAP_BUTT,
                                    BasicStroke.JOIN_BEVEL, 0.F,
                                    dashes, 0.F);
        lightStroke = new BasicStroke(dash);
        heavyStroke = new BasicStroke(2*dash);
        if (!(waypointsRight.isEmpty() && waypointsLeft.isEmpty())) {
            createWaypointStrokes(heavyStroke.getLineWidth());
        }
        font = new Font("Verdana", Font.BOLD, margin);

        Object trial = WiiKinemathicsApp.getControls().trialOption.getSelected();

        /* background */
        double bgOpacity = WiiKinemathicsApp.getControls().backgroundSlider.getNormValue();
        if (WiiKinemathicsApp.getControls().backgroundOption.isSelected())
        {
            if (trackerLeft > 0 && trackerRight > 0) {
                double tol = WiiKinemathicsApp.getControls().toleranceSlider.getValue() / 100.0;
                double correct = trackerRight*ratio;
                double e = Math.abs(correct - trackerLeft) / correct;
                e -= tol; // everything within tolerance is green (e < 0)
                e /= 2*tol; // everything beyond 2*tolerance is red (e > 1)
                Color c = getHeatMap(e);
                double inv = 1.0 - bgOpacity;
                setBackground(new Color(
                        (int)(bgOpacity * c.getRed() + inv * 255.0),
                        (int)(bgOpacity * c.getGreen() + inv * 255.0),
                        (int)(bgOpacity * c.getBlue() + inv * 255.0)));
            }
        } else {
            setBackground(Color.WHITE);
        }

        /* grid */
        double gridOpacity = WiiKinemathicsApp.getControls().gridSlider.getNormValue();
        if (gridOpacity > 0) {
            double units = WiiKinemathicsApp.getControls().unitsSlider.getValue();
            Color c = new Color(0.F,0.F,0.F,(float)gridOpacity);
            g2.setColor(c);
            double pixels = (double)height / units;
            int startx = (getWidth() / 2) % (int)pixels;
            for (int i=0; i < (getWidth()-startx)/pixels; i++) {
                int x = startx + (int)Math.floor(i*pixels);
                g2.drawLine(x, 0, x, getHeight());
            }
            int j=0;
            for (double i=0; i<=units; i+=1.0) {
                int y = margin + (int)Math.floor((double)height*(1.0 - i/units));
                g2.setColor(c);
                g2.drawLine(0, y, getWidth(), y);
                if (WiiKinemathicsApp.getControls().yTicsOption.isSelected()) {
                    g2.setFont(new Font(g2.getFont().getFontName(), g2.getFont().getStyle(), 14 ));
                    FontMetrics fm = g2.getFontMetrics();
                    String msg = String.valueOf(j);
                    Rectangle2D msgRect = fm.getStringBounds(msg, g2);
                    g2.setColor(Color.BLACK);
                    g2.drawString(msg, margin/2, y + (int)(0.5*msgRect.getHeight()));
                }
                j++;
            }
        }

        /* baseline */
        g2.setColor(Color.BLACK);
        g2.drawLine(margin, margin + height, margin + width, margin + height);

        /* waypoints */
        g2.setColor(WAYPOINT_COLOR);
        for (int i=0; i<waypointsLeft.size(); i++) {
            g2.setStroke(waypointStrokes[i%waypointStrokes.length]);
            drawWaypoint(g2, xLeft, waypointsLeft.get(i),
                    WiiKinemathicsApp.getApplication().motionDriver.getLeftLabel(i));
        }
        for (int i=0; i<waypointsRight.size(); i++) {
            g2.setStroke(waypointStrokes[i%waypointStrokes.length]);
            drawWaypoint(g2, xRight, waypointsRight.get(i),
                    WiiKinemathicsApp.getApplication().motionDriver.getRightLabel(i));
        }

        Object option;

        /* targets */
        if (trial == ControlPanel.TRIAL_TIMED)
        {
            option = WiiKinemathicsApp.getControls().rTargetOption.getSelected();
            if (option == ControlPanel.TARGET_ON) {
                g2.setColor(TARGET_COLOR);
            } else if (option == ControlPanel.TARGET_FADE) {
                g2.setColor(new Color(
                    TARGET_COLOR.getRed(),
                    TARGET_COLOR.getGreen(),
                    TARGET_COLOR.getBlue(),
                    targetAlpha));
            }
            drawHeightline(g2, xRight, targetRight, ControlPanel.HEIGHTLINE_TARGET);
            drawTarget(g2, xRight, targetRight);

            option = WiiKinemathicsApp.getControls().lTargetOption.getSelected();
            if (option == ControlPanel.TARGET_ON) {
                g2.setColor(TARGET_COLOR);
            } else if (option == ControlPanel.TARGET_FADE) {
                g2.setColor(new Color(
                    TARGET_COLOR.getRed(),
                    TARGET_COLOR.getGreen(),
                    TARGET_COLOR.getBlue(),
                    targetAlpha));
            }
            drawHeightline(g2, xLeft, targetLeft, ControlPanel.HEIGHTLINE_TARGET);
            drawTarget(g2, xLeft, targetLeft);
        }

        /* trackers */

        g2.setColor(TRACKER_COLOR);
        option = WiiKinemathicsApp.getControls().rTrackerOption.getSelected();
        if (option == ControlPanel.TRACKER_CROSSHAIRS) {
            drawHeightline(g2, xRight, trackerRight, ControlPanel.HEIGHTLINE_TRACKER);
            drawCrosshairs(g2, xRight, trackerRight);
        } else if (option == ControlPanel.TRACKER_ACCURACY) {
            double e = 1.0 - (trackerRight - targetRight) / targetRight;
            e /= (double)WiiKinemathicsApp.getControls().toleranceSlider.getValue();
            g2.setColor(getHeatMap(e));
            drawHeightline(g2, xRight, trackerRight, ControlPanel.HEIGHTLINE_TRACKER);
            drawCrosshairs(g2, xRight, trackerRight);
        } else if (option == ControlPanel.TRACKER_HEIGHT) {
            double h = trackerRight*WiiKinemathicsApp.getControls().unitsSlider.getValue();
            drawNumber(g2, xRight, trackerRight, String.format("%.1f", h));
        } else if (option == ControlPanel.TRACKER_RATIO) {
            if (trackerLeft > 0) {
                double r = (trackerRight / trackerLeft) * WiiKinemathicsApp.getControls().leftValue.getValue();
                drawNumber(g2, xRight, trackerRight, String.format("%.1f", r));
            }
        }

        g2.setColor(TRACKER_COLOR);
        option = WiiKinemathicsApp.getControls().lTrackerOption.getSelected();
        if (option == ControlPanel.TRACKER_CROSSHAIRS) {
            drawHeightline(g2, xLeft, trackerLeft, ControlPanel.HEIGHTLINE_TRACKER);
            drawCrosshairs(g2, xLeft, trackerLeft);
        } else if (option == ControlPanel.TRACKER_ACCURACY) {
            double e = 1.0 - (trackerLeft - targetLeft) / targetLeft;
            e /= (double)WiiKinemathicsApp.getControls().toleranceSlider.getValue();
            g2.setColor(getHeatMap(e));
            drawHeightline(g2, xLeft, trackerLeft, ControlPanel.HEIGHTLINE_TRACKER);
            drawCrosshairs(g2, xLeft, trackerLeft);
        } else if (option == ControlPanel.TRACKER_HEIGHT) {
            double h = trackerLeft*WiiKinemathicsApp.getControls().unitsSlider.getValue();
            drawNumber(g2, xLeft, trackerLeft, String.format("%.1f", h));
        } else if (option == ControlPanel.TRACKER_RATIO) {
            int n = WiiKinemathicsApp.getControls().leftValue.getValue();
            drawNumber(g2, xLeft, trackerLeft, String.valueOf(n));
        }

        g2.setColor(Color.RED);
        drawCenteredString(g2, (int)(0.5*getWidth()), (int)(0.5*getHeight()), message);
    }

    private void drawCenteredString(Graphics2D g2, int x, int y, String msg) {
        g2.setFont(font);
        FontMetrics fm = g2.getFontMetrics();
        Rectangle2D msgRect = fm.getStringBounds(msg, g2);
        g2.drawString(msg,
                x - (int)(0.5*msgRect.getWidth()),
                y + (int)(0.5*msgRect.getHeight()));
    }

    private void drawCrosshairs(Graphics2D g2, int x, double val) {
        int y = (int)((1.0 - val)*height) + margin;
        g2.setStroke(heavyStroke);
        g2.drawLine(x - radius, y, x + radius, y);
        g2.drawLine(x, y - radius, x, y + radius);
    }

    private void drawNumber(Graphics2D g2, int x, double val, String num) {
        int y = (int)((1.0 - val)*height) + margin;
        drawCenteredString(g2, x, y, num);
    }

    private void drawHeightline(Graphics2D g2, int x, double val, Object flag) {
        Object option = WiiKinemathicsApp.getControls().heightlineOption.getSelected();
        if (option == flag || option == ControlPanel.HEIGHTLINE_BOTH){
            int y = (int)((1.0 - val)*height) + margin;
            g2.setStroke(dashedStroke);
            g2.drawLine(x, y, x, height + margin);
        }
    }

    private void drawTarget(Graphics2D g2, int x, double val) {
        int y = (int)((1.0 - val)*height) + margin;
        g2.setStroke(heavyStroke);
        g2.drawOval(x - radius, y - radius, 2*radius, 2*radius);
    }

    private void drawWaypoint(Graphics2D g2, int x, double val, String label) {
        int y = (int)Math.floor((1.0 - val)*height) + margin;
        g2.drawLine(x - margin, y, x + margin, y);
        drawCenteredString(g2, x - 2*margin, y, label);
    }

    private Color getHeatMap(double val) {
        if (val < 0.0) return HEATMAP_COLORS[0];
        double abs = Math.abs(val);
        if (abs > 0.99) return HEATMAP_COLORS[HEATMAP_COLORS.length - 1];
        double len = (double)(HEATMAP_COLORS.length - 1);
        double floor = Math.floor(abs*len);
        double t = len * (abs - floor / len);
        int i = (int)floor;
        int r = (int)( (1.0-t) * HEATMAP_COLORS[i].getRed() +
                        t      * HEATMAP_COLORS[i+1].getRed());
        int g = (int)( (1.0-t) * HEATMAP_COLORS[i].getGreen() +
                        t      * HEATMAP_COLORS[i+1].getGreen());
        int b = (int)( (1.0-t) * HEATMAP_COLORS[i].getBlue() +
                        t      * HEATMAP_COLORS[i+1].getBlue());
        Color c = new Color(r,g,b);
        //getLogger().info(String.format("heat map: val %g t %g color %d %d %d",
        //        val, t, r, g, b));
        return c;
    }

    private void resetMessage() {
        message = "";
        repaint();
    }

    public boolean countDown(String action, int count) {
        for (int i=count; i>0; i--) {
            message = String.format("%s in %d", action, i);
            repaint();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Logger.getLogger("WiiKinemathics").info("Interrupted.");
                resetMessage();
                return false;
            }
        }
        resetMessage();
        return true;
    }

    public void resetHeight() {
        trackerLeft = trackerRight = targetLeft = targetRight = 0.0;
    }

    public void calibrate() {
        resetHeight();
        countDown("Calibrating", 3);
    }

    public boolean abort() {
        message = "Unable to calibrate";
        repaint();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Logger.getLogger("WiiKinemathics").info("Interrupted.");
            resetMessage();
            return false;
        }
        resetMessage();
        reset();
        return true;
    }

    public void updateTracker(double left, double right) {
        //app.getLogger().info(String.format("update tracker: %f %f", left, right));
        if (WiiKinemathicsApp.getControls().monotoneOption.isSelected()) {
            trackerLeft = Math.max(trackerLeft, Math.max(0, left));
            trackerRight = Math.max(trackerRight, Math.max(0, right));
        } else {
            trackerLeft = Math.max(0, left);
            trackerRight = Math.max(0, right);
        }
        repaint();
    }

    public void updateTarget(double left, double right, double alpha) {
        targetAlpha = (int)Math.round(255.0*alpha);
        targetLeft = left;
        targetRight = right;
    }

    public void updateDriver(double left, double right) {
        trackerLeft = left;
        trackerRight = right;
    }

    public void addLeftWaypoint(double waypoint) {
        waypointsLeft.add(waypoint);
    }

    public void addRightWaypoint(double waypoint) {
        waypointsRight.add(waypoint);
    }

    public void reset() {
        waypointsLeft.clear();
        waypointsRight.clear();
        resetHeight();
        repaint();
    }

    public int getTrackHeight() {
        return getHeight() - 2*margin;
    }

    private void createWaypointStrokes(float weight) {
        float[] dashes1 = {weight, 2*weight};
        float[] dashes2 = {weight, weight, 2*weight, weight};
        float[] dashes3 = {2*weight, 2*weight};
        waypointStrokes = new BasicStroke[4];
        waypointStrokes[0] = new BasicStroke(weight);
        waypointStrokes[1] = new BasicStroke(weight, BasicStroke.CAP_BUTT,
                                    BasicStroke.JOIN_BEVEL, 0.F,
                                    dashes1, 0.F);
        waypointStrokes[2] = new BasicStroke(weight, BasicStroke.CAP_BUTT,
                                    BasicStroke.JOIN_BEVEL, 0.F,
                                    dashes2, 0.F);
        waypointStrokes[3] = new BasicStroke(weight, BasicStroke.CAP_BUTT,
                                    BasicStroke.JOIN_BEVEL, 0.F,
                                    dashes3, 0.F);
    }
}
